package org.hawk.gwt.ppc.reflect;

import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.hawk.gwt.ppc.Configuration;
import org.hawk.gwt.ppc.GWTModule;
import org.hawk.gwt.ppc.ProcessingUnitsManager;
import org.hawk.gwt.ppc.introspection.IntrospectionUtils;
import org.hawk.gwt.ppc.introspection.JavaTypeScope;
import org.hawk.gwt.ppc.introspection.SourcesRepository;
import org.hawk.gwt.ppc.loader.ProcessingUnit;
import org.hawk.gwt.ppc.parser.ClassDescriptor;
import org.hawk.gwt.ppc.parser.CompilationUnitDescriptor;
import org.hawk.gwt.ppc.parser.Descriptor;
import org.hawk.gwt.ppc.parser.DescriptorVisitor;
import org.hawk.gwt.ppc.parser.ExpressionDescriptor;
import org.hawk.gwt.ppc.parser.MethodCallDescriptor;
import org.hawk.gwt.ppc.parser.NameExpressionDescriptor;
import org.hawk.gwt.ppc.parser.OperatorExpressionDescriptor;
import org.hawk.gwt.ppc.parser.Token;
import org.hawk.gwt.ppc.reflect.lang.ClassDelegate;
import org.hawk.gwt.ppc.utils.Pattern;
import org.hawk.gwt.ppc.utils.PreprocessorException;

/**
 * Replaces all reflection calls and classes with theirs analogs from the
 * internal implementation. All {@link Object#getClass()} method calls and
 * [ClassName].class properties accesses will be wrapped with internal
 * {@link ClassDelegate#forClass(Class)} calls. Java class will be used as
 * parameter.
 * 
 * @author alex.bereznevatiy@gmail.com
 */
public class ReflectionCallsProcessor implements ProcessingUnit {
	
	private static final String PREFIX = ClassDelegate.class.getName() + ".forClass(";
	private static final String FOR_NAME_PREFIX = ClassDelegate.class.getName();
	
	private static final String SUFFIX = ")";
	private static final String FOR_NAME_SUFFIX = ".toClass()";
	
	private static final Set<String> reflectionMethodNames;

	static {
		Set<String> rez = new HashSet<String>();
		rez.add("forName");
		rez.add("newInstance");
		rez.add("isInstance");
		rez.add("isAssignableFrom");
		rez.add("getClassLoader");
		rez.add("getPackage");
		rez.add("getInterfaces");
		rez.add("getModifiers");
		rez.add("getSimpleName");
		rez.add("getCanonicalName");
		rez.add("isAnnotation");
		rez.add("isAnonymousClass");
		rez.add("getFields");
		rez.add("getMethods");
		rez.add("getConstructors");
		rez.add("getField");
		rez.add("getMethod");
		rez.add("getConstructor");
		rez.add("getDeclaredFields");
		rez.add("getDeclaredMethods");
		rez.add("getDeclaredConstructors");
		rez.add("getDeclaredField");
		rez.add("getDeclaredMethod");
		rez.add("getDeclaredConstructor");
		rez.add("asSubclass");
		rez.add("getTypeParameters");
		rez.add("getGenericInterfaces");
		rez.add("getGenericSuperclass");

		reflectionMethodNames = Collections.unmodifiableSet(rez);
	}
	
	private List<Pattern> pattern;
	private SourcesRepository repository;
	private Set<String> processed;
	private Set<Descriptor> forNames;

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#configure(org.hawk.gwt.ppc.Configuration, org.hawk.gwt.ppc.ProcessingUnitsManager)
	 */
	public void configure(Configuration config, ProcessingUnitsManager manager) {
		repository = manager.getRepository();
		pattern = new LinkedList<Pattern>();
		processed = new HashSet<String>();
		
		String defaultPattern = config.get(Configuration.REFLECTION_PARAMETER);
		String fullPattern = config.get(Configuration.PREPROCESS_PARAMETER, defaultPattern);
		if (fullPattern == null || fullPattern.trim().length() == 0) {
			return;
		}
		String[] parts = fullPattern.split(",");
		
		for (String p : parts) {
			pattern.add(config.getNewPattern(p));
		}
		pattern = Collections.unmodifiableList(pattern);
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#processModule(org.hawk.gwt.ppc.GWTModule)
	 */
	public void processModule(GWTModule module) throws PreprocessorException {
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#processCompilationUnit(java.lang.String)
	 */
	public void processCompilationUnit(String compilationUnitName)
			throws PreprocessorException {
		CompilationUnitDescriptor compilationUnit = repository.findCompilationUnit(compilationUnitName);
		
		for (ClassDescriptor clazz : compilationUnit.getTopClasses()) {
			processClass(clazz);
		}
	}

	private void processClass(ClassDescriptor clazz) throws PreprocessorException {
		if (processed.contains(clazz.getName())) {
			return;
		}
		forNames = new HashSet<Descriptor>();
		clazz.visit(new DescriptorVisitor() {
			
			public void visit(Descriptor descriptor) throws PreprocessorException {
				if (descriptor instanceof MethodCallDescriptor) {
					checkMethod((MethodCallDescriptor)descriptor);
				}
			}
		});
		forNames = null;
		processed.add(clazz.getName());
		repository.markChanged(clazz.getName());
	}

	private void checkMethod(MethodCallDescriptor descriptor) throws PreprocessorException {
		if (!reflectionMethodNames.contains(descriptor.getName())) {
			return;
		}
		
		ExpressionDescriptor expression = descriptor.getTarget();
		
		if (expression == null) {
			return;
		}
		
		if (forNames.contains(descriptor)) {
			return;// already processed
		}
		
		JavaTypeScope type = IntrospectionUtils.evaluateReturnTypeOfExpression(expression, repository);
		
		if (type == null || !type.getName().equals(Class.class.getName())) {
			return;
		}
		
		if (descriptor.getName().equals("forName") && parseForName(descriptor)) {
			descriptor.getLastToken().getRaw().append(FOR_NAME_SUFFIX);
			return;
		}
		
		if (hasForNameTarget(descriptor)) {
			parseForName((MethodCallDescriptor)descriptor.getTarget());
			forNames.add(descriptor.getTarget());
			return;
		}
		
		expression.getFirstToken().getRaw().insert(0, PREFIX);
		expression.getLastToken().getRaw().append(SUFFIX);
	}

	private boolean hasForNameTarget(MethodCallDescriptor descriptor) throws PreprocessorException {
		if (!(descriptor.getTarget() instanceof MethodCallDescriptor)) {
			return false;
		}
		MethodCallDescriptor potentialForName = (MethodCallDescriptor) descriptor.getTarget();
		if (!potentialForName.getName().equals("forName")) {
			return false;
		}
		if (getFirstClassTypeToken(potentialForName.getTarget()) != null) {
			return true;
		}
		return IntrospectionUtils.evaluateReturnTypeOfExpression(potentialForName.getTarget(), 
				repository).getName().equals(Class.class.getName());
	}

	private boolean parseForName(MethodCallDescriptor descriptor) {
		if (!eraiseClass(descriptor)) {
			return false;
		}
		descriptor.getTarget().getFirstToken().getRaw().append(FOR_NAME_PREFIX);
		return true;
	}

	private boolean eraiseClass(MethodCallDescriptor descriptor) {
		Descriptor target = descriptor.getTarget();
		
		Token start = getFirstClassTypeToken(target);
		if (start == null) {
			return false;
		}
		Token end = target.getLastToken();
		while (start != end) {
			start.getRaw().setLength(0);
			start = start.next();
		}
		start.getRaw().setLength(0);
		return true;
	}
	
	private Token getFirstClassTypeToken(Descriptor target) {
		if (target instanceof NameExpressionDescriptor) {
			if (((NameExpressionDescriptor)target).getName().equals(Class.class.getSimpleName())) {
				return target.getFirstToken();
			}
			return null;
		}
		if (!(target instanceof OperatorExpressionDescriptor)) {
			return null;
		}
		OperatorExpressionDescriptor operator = (OperatorExpressionDescriptor) target;
		if (!operator.getOperator().equals(".") || 
			!(operator.getLeftOperand() instanceof OperatorExpressionDescriptor) || 
			!(operator.getRightOperand() instanceof NameExpressionDescriptor)) {
			return null;
		}
		
		NameExpressionDescriptor className = (NameExpressionDescriptor)operator.getRightOperand();
		
		if (!className.getName().equals(Class.class.getSimpleName())) {
			return null;
		}
		
		operator = (OperatorExpressionDescriptor) operator.getLeftOperand();
		
		if (!operator.getOperator().equals(".") || 
			!(operator.getLeftOperand() instanceof NameExpressionDescriptor) || 
			!(operator.getRightOperand() instanceof NameExpressionDescriptor)) {
			return null;
		}
		
		NameExpressionDescriptor javaName = (NameExpressionDescriptor)operator.getLeftOperand();
		NameExpressionDescriptor langName = (NameExpressionDescriptor)operator.getRightOperand();
		
		if (!javaName.getName().equals("java") ||
				!langName.getName().equals("lang")) {
			return null;
		}
		
		return javaName.getFirstToken();
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#processEntryPoint(java.lang.String)
	 */
	public void processEntryPoint(String entryPoint)
			throws PreprocessorException {
	}

	/* (non-Javadoc)
	 * @see org.hawk.gwt.ppc.loader.ProcessingUnit#acceptResource(java.lang.String)
	 */
	public boolean acceptResource(String resource) {
		for (Pattern p : pattern) {
			if (p.match(resource)) {
				return true;
			}
		}
		return false;
	}
}
